Feat(text tool): Add import custom fonts - #9091
Conversation
…t-custom-fonts # Conflicts: # invokeai/app/api/routers/utilities.py # invokeai/app/services/config/config_default.py # invokeai/frontend/web/public/locales/en.json # invokeai/frontend/web/src/services/api/endpoints/utilities.ts
|
@Pfannkuchensack What changed:
On the auth question: this router is not mounted behind a parent FastAPI dependency. Confirmed: I addressed that by making custom font availability reactive:
So this is no longer relying on an implicit re-render path. |
|
Addressed the actionable parts. Changed:
Already fixed in current branch:
Notable non-blockers: Implemented fixes for items 2, 3, and 4. Leaving item 1 as a follow-up. Leaving item 1 as a follow-up because changing custom font IDs from heuristic representative paths to stable IDs/content hashes would be a persistence/schema decision. It affects saved canvas state compatibility and may require migration or fallback handling for existing
Note: |
|
Update on the CI note: #9457 fixed the missing |
…t-custom-fonts # Conflicts: # invokeai/frontend/web/public/locales/en.json
…t-custom-fonts # Conflicts: # uv.lock
| useEffect(() => { | ||
| if (!hasUserFontErrors) { | ||
| autoRetryAttemptsRef.current = 0; | ||
| return; | ||
| } | ||
| if (typeof window === 'undefined' || autoRetryAttemptsRef.current >= 2) { | ||
| return; | ||
| } | ||
|
|
||
| const delayMs = autoRetryAttemptsRef.current === 0 ? 1000 : 3000; | ||
| const timeout = window.setTimeout(() => { | ||
| autoRetryAttemptsRef.current += 1; | ||
| setFontSyncVersion((version) => version + 1); | ||
| }, delayMs); | ||
|
|
||
| return () => { | ||
| window.clearTimeout(timeout); | ||
| }; | ||
| }, [fontSyncVersion, hasUserFontErrors]); |
There was a problem hiding this comment.
Each retry changes the readiness state from error to pending. That makes hasUserFontErrors false and resets autoRetryAttemptsRef to 0.
A permanently broken font therefore retries roughly every second indefinitely instead of stopping after two attempts. The retry count should survive the error → pending → error transition and reset only after a successful load or an explicit user action.
| fonts: list[UserFont] = [] | ||
| for _, candidates in sorted(family_candidates.items(), key=lambda kv: kv[0]): | ||
| _, selected_relative, selected_family, _, _ = min(candidates, key=lambda c: _candidate_score(c[3], c[4], c[0])) | ||
| faces_by_variant: dict[tuple[int, str], tuple[Path, str, str, int, str]] = {} | ||
| for candidate in candidates: | ||
| variant_key = (candidate[3], candidate[4]) | ||
| current = faces_by_variant.get(variant_key) | ||
| if current is None or _candidate_score(candidate[3], candidate[4], candidate[0]) < _candidate_score( | ||
| current[3], current[4], current[0] | ||
| ): | ||
| faces_by_variant[variant_key] = candidate | ||
|
|
||
| faces = [ | ||
| UserFontFace( | ||
| path=relative, | ||
| url=f"api/v1/utilities/fonts/{quote(relative)}", | ||
| weight=weight, | ||
| style=style, | ||
| ) | ||
| for (weight, style), (_, relative, _, _, _) in sorted( | ||
| faces_by_variant.items(), key=lambda item: (item[0][1] != "normal", abs(item[0][0] - 400), item[0][0]) | ||
| ) | ||
| ] | ||
|
|
||
| fonts.append( | ||
| UserFont( | ||
| id=f"user:{selected_relative}", | ||
| family=selected_family, | ||
| label=selected_family, | ||
| path=selected_relative, | ||
| url=f"api/v1/utilities/fonts/{quote(selected_relative)}", | ||
| faces=faces, | ||
| ) | ||
| ) | ||
|
|
||
| return UserFontsResponse(fonts=fonts) |
There was a problem hiding this comment.
Derives a font family’s persisted ID from whichever face wins _candidate_score. Adding or removing a face can change that ID, leaving the stored selection missing and causing commits to time out until another font is selected.
There was a problem hiding this comment.
The TEXT_FONT_IDS constant was removed in this PR and no longer exists
|
Addressed all three comments in 88c937f.
Added regression coverage for bounded retries, legacy path aliases, and stable IDs when the preferred face changes. |
joshistoast
left a comment
There was a problem hiding this comment.
should be good to go after this
| return; | ||
| } | ||
|
|
||
| primeUserFontReadiness(userFonts ?? [], loadedUserFontFaces); |
There was a problem hiding this comment.
Treats initial userFonts === undefined as an empty list. After RTK cache expiry, remounting prunes all cached FontFaces and temporarily switches to fallback fonts before reloading. Guard font synchronization until query data is available.
There was a problem hiding this comment.
Good catch. Font synchronization and custom stack updates are now guarded until the query has returned actual data.
While userFonts is undefined, the existing FontFace registry, custom stacks, and retry state are preserved. A confirmed empty response still prunes fonts normally.

Summary
Add support for custom text-tool fonts loaded from
invokeai/Fonts.This feature lets users add their own font files without replacing the built-in font set. The backend now discovers and serves fonts from
invokeai/Fonts, creates the directory andREADME.txtautomatically, and parses font metadata to show clean family names. The frontend loads these fonts into the text tool, shows them separately from built-in fonts, and keeps built-in fonts as fallback when no custom fonts are available.Related Issues / Discussions
N/A
QA Instructions
.ttf,.otf,.woff, or.woff2files toinvokeai/Fontsinvokeai/Fontsis emptyinvokeai/frontend/web/src/services/api/schema.tswas regenerated after adding the new utilities endpointsMerge Plan
Standard merge.
Checklist
What's Newcopy (if doing a release after this PR)